package moltencore.game;

import moltencore.exception.CoreRuntimeException;
import moltencore.game.exception.IllegalOperationException;
import spark.utility.Clock;
import spark.utility.FrameBreak;
import spark.utility.IncrementalLongIdFactory;
import spark.network.IPlayerListEventListener;
import spark.math.Vector2;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;

import moltencore.network.game.exception.FullGameException;
import moltencore.game.bubble.Bubble;
import moltencore.player.PlayerList;
import moltencore.player.exception.PlayerAlreadyRegisteredException;
import moltencore.player.exception.PlayerNameAlreadyExistsException;
import moltencore.config.Config;
import org.apache.log4j.Logger;

public abstract class Game implements Runnable, ICannonFireListener {
	private static Logger logger = Logger.getLogger(Game.class);

	// IMPLEMENTATION /// TODO
	public int teamSize;

	private IncrementalLongIdFactory clientIdFactory = new IncrementalLongIdFactory();
	private FrameBreak frameBreak;

	private long hostIdentification;
	protected Collection<Field> fields;
	protected LinkedList<Field> winnerFields = new LinkedList<Field>();
	protected LinkedList<Field> loserFields = new LinkedList<Field>();

	private PlayerList<Player> players;
	private State state = State.Created;
	private LinkedList<IGameEventListener> listeners = new LinkedList<IGameEventListener>();

	public Game(long hostIdentification) {
		this.hostIdentification = hostIdentification;
		this.frameBreak = new FrameBreak(Config.getInstance().getFrameBreak());
	}

	@Override
	public void run() {
		if(getState() != State.ReadyToStart) {
			throw new CoreRuntimeException("Game is not ready to start");
		}
		preparePlayersForPlay();
		setState(State.Started);
		Clock clock = new Clock();
		clock.reset();
		gameStartHook();
		while(getState() == State.Started) {
			clock.update();
			breakGame(clock.getElapsedMilliseconds());
			doFrame(clock.getElapsedSeconds());
		}
	}

	protected void doFrame(float elapsedSeconds) {
		for(Field field : getFields()) {
			//if this field has won/lost don't update it
			if(winnerFields.contains(field) || loserFields.contains(field)) {
				continue;
			}
			for(Player player : field.getOwners()) {
				player.update(elapsedSeconds);
			}
			field.update(elapsedSeconds);
			reflectOnFieldUpdate(field);
			handlePossibleGameEnd();
			if(State.Ended == getState()) {
				break;
			}
		}
		updateHook(elapsedSeconds);
	}

	protected void updateHook(float elapsedSeconds) {
	}

	protected void gameStartHook() {
	}

	protected abstract void reflectOnFieldUpdate(Field field);

	protected abstract void handlePossibleGameEnd();

	protected void breakGame(float elapsedMs) {
		frameBreak.breakFrame(elapsedMs);
	}

	public void setFields(Collection<Field> fields) {
		if(state != State.Created) {
			throw new IllegalOperationException("Game has already begun");
		}
		this.fields = fields;
	}

	public void setPlayers(PlayerList<Player> players) {
		if(state != State.Created) {
			throw new IllegalOperationException("Game has already begun");
		}
		this.players = players;
	}

	/**
	 * All necessary preparations needed to start the game followed by game start (asynchronously).
	 */
	public void commence() {
//		Iterator<Field> fieldIterator = fields.iterator();
//		//assign one player per field
//		for(Player player : players.getOnlinePlayers()) {
//			player.setField(fieldIterator.next());
//		}
//		//left over fields are not needed and can be removed from the game
//		while(fieldIterator.hasNext()) {
//			fieldIterator.remove();
//		}
		state = State.PreparingForStart;
		notifyListenersGameStarting();
	}

	/**
	 * Add a new player to the game. The player will have a status of {@link Player.State#Joined}.
	 *
	 * @param name        Player's name.
	 * @param session     Player's session.
	 * @param cannonConfiguration   Player's cannon properties.
	 * @param bubbleSetId Id of the players bubble set. Only needed for visualization.
	 * @return Player that has been added to the game.
	 * @throws FullGameException if the game has already reached the limit of players it can hold.
	 * @throws PlayerAlreadyRegisteredException
	 *                           If a player with this {@code session} has already joined this game.
	 * @throws PlayerNameAlreadyExistsException
	 *                           If a player with this {@code name} has alraedy joined this game.
	 */
/*	public Player addPlayer(String name, byte bubbleSetId, long session, CannonConfiguration cannonConfiguration) throws FullGameException, PlayerAlreadyRegisteredException, PlayerNameAlreadyExistsException {
		if(players.size() == settings.getPlayerLimit()) {
			throw new FullGameException();
		}
		Player player = new Player(name, bubbleSetId, session, clientIdFactory.getNextId(), cannonConfiguration);
		players.login(player);
		for(IGameEventListener gameEventListener : listeners) {
			player.addGameEventListener(gameEventListener);
		}
		notifyListenersPlayerJoined(player);
		return player;
	}*/

	/**
	 * Sets the status of the player with the corresponding session to {@link Player.State#ReadyToPlay}.
	 * Notifies listeners that the game is ready to be started once all players are ready.
	 *
	 * @param session Session of a player who is in the game. The provided session must correspond to an ingame player.
	 */
	public void playerReady(long session) {
		players.getPlayer(session).setState(Player.State.ReadyToPlay);
		if(allPlayersReady()) {
			logger.info("All players are ready, notifying.");
			state = State.ReadyToStart;
			//start the game
			new Thread(this).start();
			notifyListenersGameReadyToStart();
		}
	}

	/**
	 * Signifies that player with {@code session} wants to fire his cannon.
	 *
	 * @param session Session of a player who is in the game. The provided session must correspond to an ingame player.
	 */
	public void playerFire(long session) {
		players.getPlayer(session).getActionMap().fire();
	}

	/**
	 * Signifies that player with {@code session} wants to rotate his cannon.
	 *
	 * @param session   Session of a player who is in the game. The provided session must correspond to an ingame player.
	 * @param direction Direction in which the player wants to rotate his cannon.
	 */
	public void playerStartRotateCannon(long session, Cannon.Direction direction) {
		players.getPlayer(session).getActionMap().startRotating(direction);
	}

	/**
	 * A player stops rotating his cannon.
	 *
	 * @param session Session of a player who is in the game. The provided session must correspond to an ingame player.
	 */
	public void playerStopRotatingCannon(long session) {
		players.getPlayer(session).getActionMap().stopRotating();
	}

	private void notifyListenersGameReadyToStart() {
		for(IGameEventListener gameEventListener : listeners) {
			gameEventListener.gameReadyToStart(this);
		}
	}

	private void notifyListenersGameStarting() {
		for(IGameEventListener gameEventListener : listeners) {
			gameEventListener.gameStarting(this, teamSize);
		}
	}

	protected void notifyListenersGameEnded(Collection<Field> winners, Collection<Field> losers) {
		for(IGameEventListener gameEventListener : listeners) {
			gameEventListener.gameEnded(winners, losers);
		}
	}

	private boolean allPlayersReady() {
		for(Player player : players.getOnlinePlayers()) {
			if(player.getState() != Player.State.ReadyToPlay) {
				return false;
			}
		}
		return true;
	}

	@Override
	public void cannonFired(Player cannonOwner) {
		Bubble nextBubble = cannonOwner.getField().createRandomBubble();
		cannonOwner.getCannon().load(nextBubble);
	}

	/**
	 * Sets the status of each player to playing and gives them their initial cannonballs
	 */
	protected void preparePlayersForPlay() {
		setAllPlayersPlaying();
		//load up the cannons with 1 bubble each and register the game as cannonfire listener for furher reloads
		for(Player player : players.getOnlinePlayers()) {
			player.setCannonFireListener(this);
			Bubble firstBubble = player.getField().createRandomBubble();
			player.getCannon().load(firstBubble);
		}
	}

	/**
	 * Sets the status of every player to {@link Player.State#Playing}
	 */
	private void setAllPlayersPlaying() {
		for(Player player : players.getOnlinePlayers()) {
			player.setState(Player.State.Playing);
		}
	}

	public LinkedList<Player> getCurrentPlayers() {
		LinkedList<Player> currentPlayers = new LinkedList<Player>();
		for(Iterator<Player> playerIterator = players.getOnlinePlayers().iterator(); playerIterator.hasNext();) {
			currentPlayers.add(playerIterator.next());
		}
		return currentPlayers;
	}

	protected PlayerList<Player> getPlayers() {
		return players;
	}

	protected Collection<Field> getFields() {
		return fields;
	}

	public State getState() {
		return state;
	}

	public void setState(State state) {
		this.state = state;
	}

	public long getHostIdentification() {
		return hostIdentification;
	}

	public void addGameEventListener(IGameEventListener listener) {
		if(!listeners.contains(listener)) {
			listeners.add(listener);
		}
		if(players != null) {
			for(Player player : players.getOnlinePlayers()) {
				player.addGameEventListener(listener);
			}
		}
		if(fields != null) {
			for(Field field : fields) {
				field.addGameEventListener(listener);
			}
		}
	}

	public void removeGameEventListener(IGameEventListener listener) {
		listeners.remove(listener);
		if(players != null) {
			for(Player player : players.getOnlinePlayers()) {
				player.removeGameEventListener(listener);
			}
		}
		if(fields != null) {
			for(Field field : fields) {
				field.removeGameEventListener(listener);
			}
		}
	}

	protected LinkedList<IGameEventListener> getListeners() {
		return listeners;
	}

	//IMPLEMENTATION

	public long getFieldCount() {
		return fields.size();
	}

	public long getPlayerId(int playerIndex) {
		Iterator<Player> it = players.getOnlinePlayers().iterator();
		Player player = it.next();
		for(int i = 0; i < playerIndex; i++) {
			player = it.next();
		}
		return player.getClientId();
	}

	public Vector2 getPlayerCannonPosition(int playerIndex) {
		Iterator<Player> it = players.getOnlinePlayers().iterator();
		Player player = it.next();
		for(int i = 0; i < playerIndex; i++) {
			player = it.next();
		}
		return player.getCannon().getPosition();
	}

	public Vector2 getPlayerCannonAimVector(int playerIndex) {
		Iterator<Player> it = players.getOnlinePlayers().iterator();
		Player player = it.next();
		for(int i = 0; i < playerIndex; i++) {
			player = it.next();
		}
		return player.getCannon().getTarget();
	}

/*	public Collection<Player> getOnlinePlayers() {
		return players.getOnlinePlayers();
	}*/

	public static enum State {
		Created,
		PreparingForStart,
		ReadyToStart,
		Started,
		Ended
	}

	public static enum Mode {
		DeathMatch(0, "Deathmatch", Field.PushType.Row),
		TimeAttack(1, "Timeattack", Field.PushType.Row),
		ShootEmUp(2, "Shoot 'em up", Field.PushType.NeverPush);

		private byte type;
		private String name;
		private Field.PushType pushType;

		private Mode(int type, String name, Field.PushType pushType) {
			this.type = (byte) type;
			this.name = name;
			this.pushType = pushType;
		}

		public byte getType() {
			return type;
		}

		public Field.PushType getPushType() {
			return pushType;
		}

		public static Mode fromType(byte type) {
			if(type == DeathMatch.getType()) {
				return DeathMatch;
			}
			else if(type == TimeAttack.getType()) {
				return TimeAttack;
			}
			else if(type == ShootEmUp.getType()) {
				return ShootEmUp;
			}
			else {
				throw new RuntimeException("Could not match game mode: " + type);
			}
		}

		@Override
		public String toString() {
			return name;
		}
	}
}
